package jp.dodododo.reloadable.filter;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import jp.dodododo.reloadable.file.StateFile;
import jp.dodododo.reloadable.net.URLClassLoaderEx;
import jp.dodododo.reloadable.servlet.ServletContextListenerWrapperContainer;
import jp.dodododo.reloadable.servlet.ServletWrapperContainer;
import jp.dodododo.reloadable.util.ContextClassLoaderUtils;
import jp.dodododo.reloadable.util.StringUtils;

public class ReloadableFilter extends SessionFilter {

	private File webInfDir;

	private File webLibDir;

	private File webClassesDir;

	private File customClassesDir;

	private StateFile stateFile;

	private ClassLoader lastLoader;

	private ServletContext servletContext;

	private List<Pattern> ignoreFilePatternList;

	private Set<String> notRemoveServletContextAttributeNames = null;

	private static final String CUSTOM_CLASSES_DIR_KEY = ReloadableFilter.class.getName() + ".customClassesDir";

	private static final String IGNORE_FILE_PATTERN_KEY = ReloadableFilter.class.getName() + ".ignoreFilePattern";

	/**
	 * @IgnorePoint
	 */
	@Override
	public synchronized void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
			ServletException {
		ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();

		try {
			boolean initFlag = false;
			boolean isChanged = stateFile.isChange() == true;

			ServletContextEvent event = new ServletContextEvent(servletContext);
			if (isChanged == true || lastLoader == null) {
				initFlag = true;
				initState();

				FilterWrapperContainer.destroyAll();
				ServletWrapperContainer.destroyAll();
				ServletContextListenerWrapperContainer.destroyAll(event);
				if (notRemoveServletContextAttributeNames == null) {
					saveServletContextAttributeNames();
				}
				cleanServletContext();

				clearAssertionStatus();
				lastLoader = null;

				lastLoader = createNewClassLoader(oldClassLoader);
			}

			Thread.currentThread().setContextClassLoader(lastLoader);
			ContextClassLoaderUtils.setContextClassLoader(lastLoader);

			if (initFlag == true) {
				ServletContextListenerWrapperContainer.initAll(event);
				ServletWrapperContainer.initAll();
				FilterWrapperContainer.initAll();
			}
			super.doFilter(req, res, chain);
		} finally {
			ContextClassLoaderUtils.setContextClassLoader(oldClassLoader);
			Thread.currentThread().setContextClassLoader(oldClassLoader);
			oldClassLoader = null;
		}
	}

	private void saveServletContextAttributeNames() {
		this.notRemoveServletContextAttributeNames = new HashSet<String>();

		@SuppressWarnings("unchecked")
		Enumeration<String> attributeNames = servletContext.getAttributeNames();
		for (; attributeNames.hasMoreElements();) {
			String name = attributeNames.nextElement();
			notRemoveServletContextAttributeNames.add(name);
		}

	}

	private void clearAssertionStatus() {
		if (lastLoader != null) {
			lastLoader.clearAssertionStatus();
		}
	}

	private ClassLoader createNewClassLoader(ClassLoader oldClassLoader) throws IOException, MalformedURLException {
		URL[] jarURLs = getJarURLs();
		List<URL> urls = new ArrayList<URL>();
		if (customClassesDir != null) {
			urls.add(customClassesDir.toURI().toURL());
		}
		urls.add(webClassesDir.toURI().toURL());
		for (int i = 0; i < jarURLs.length; i++) {
			urls.add(jarURLs[i]);

		}
		return new URLClassLoaderEx(urls.toArray(new URL[urls.size()]), oldClassLoader);
	}

	private static URL[] getJarURLs(File libDir) throws IOException {
		File[] jarPaths = libDir.listFiles();
		URL[] jarURLs = toURLs(jarPaths);
		return jarURLs;
	}

	private URL[] getJarURLs() throws IOException {
		return getJarURLs(webLibDir);
	}

	private static URL[] toURLs(File[] jarPaths) throws IOException {
		URL[] ret = new URL[jarPaths.length];
		for (int i = 0; i < jarPaths.length; i++) {
			ret[i] = jarPaths[i].toURI().toURL();
		}
		return ret;
	}

	@Override
	public void init(FilterConfig config) throws ServletException {
		super.init(config);
		servletContext = config.getServletContext();

		ServletContext servletContext = config.getServletContext();
		webInfDir = new File(servletContext.getRealPath("WEB-INF"));
		webClassesDir = new File(servletContext.getRealPath("WEB-INF/dev/classes"));
		webLibDir = new File(servletContext.getRealPath("WEB-INF/dev/lib"));

		String dir = config.getInitParameter(CUSTOM_CLASSES_DIR_KEY);
		if (StringUtils.isEmpty(dir) == false) {
			File customClassesDir = new File(servletContext.getRealPath(dir));
			if (customClassesDir.exists() == true) {
				this.customClassesDir = customClassesDir;
			} else {
				System.out.println("WARN : " + dir + " was not found.");
			}
		}
		String ignoreFilePattern = config.getInitParameter(IGNORE_FILE_PATTERN_KEY);
		String[] patterns = ignoreFilePattern.split("\n|\r|,");
		ignoreFilePatternList = new ArrayList<Pattern>();
		ignoreFilePatternList.add(Pattern.compile("web.xml"));
		for (int i = 0; i < patterns.length; i++) {
			String string = patterns[i].trim();
			if (StringUtils.isEmpty(string) == true) {
				continue;
			}
			char c = string.charAt(0);
			if (c == '*') {
				string = "." + string;
			}
			Pattern pattern = Pattern.compile(string);
			if (ignoreFilePatternList.contains(pattern) == false) {
				ignoreFilePatternList.add(pattern);
			}
		}

		initState();
	}

	private void initState() {
		stateFile = new StateFile(webInfDir, ignoreFilePatternList);
	}

	@Override
	public void destroy() {
		super.destroy();

		ignoreFilePatternList = null;
		customClassesDir = null;
		webLibDir = null;
		webClassesDir = null;
		stateFile = null;
		servletContext = null;

		clearAssertionStatus();
		lastLoader = null;
	}

	private void cleanServletContext() {
		@SuppressWarnings("unchecked")
		Enumeration<String> attributeNames = servletContext.getAttributeNames();
		for (; attributeNames.hasMoreElements();) {
			String name = attributeNames.nextElement();
			// if (notRemoveServletContextAttributeNames.contains(name) ==
			// false) {
			servletContext.removeAttribute(name);
			// }
		}
	}

}
